package com.tgr.shortLink.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.Week;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tgr.shortLink.project.common.convention.exception.ClientException;

import com.tgr.shortLink.project.common.convention.exception.ServiceException;
import com.tgr.shortLink.project.common.enums.VailDateTypeEnum;
import com.tgr.shortLink.project.dao.entity.*;
import com.tgr.shortLink.project.dao.mapper.*;
import com.tgr.shortLink.project.dto.req.LinkCreateReqDTO;
import com.tgr.shortLink.project.dto.req.LinkPageReqDTO;
import com.tgr.shortLink.project.dto.req.LinkUpdateReqDTO;
import com.tgr.shortLink.project.dto.resp.LinkCreateRespDTO;
import com.tgr.shortLink.project.dto.resp.LinkGroupQueryRespDTO;
import com.tgr.shortLink.project.dto.resp.LinkPageRespDTO;
import com.tgr.shortLink.project.service.LinkService;
import com.tgr.shortLink.project.toolkit.FaviconUtil;
import com.tgr.shortLink.project.toolkit.HashUtil;
import com.tgr.shortLink.project.toolkit.LinkUtil;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static com.tgr.shortLink.project.common.constant.RedisKeyConstant.*;
import static com.tgr.shortLink.project.common.constant.ShortLinkConstant.AMAP_REMOTE_URL;
import static com.tgr.shortLink.project.common.convention.errorCode.BaseErrorCode.TARGET_WEBSITE_TITLE_ERROR;

/**
 * 描述：短链接服务层
 * 作者：小陶不慌张
 * 文件：LinkServiceImpl
 * 日期：2024/1/16 11:09
 */
@RequiredArgsConstructor
@Slf4j
@Service
public class LinkServiceImpl extends ServiceImpl<LinkMapper, LinkDO> implements LinkService {
    private final RBloomFilter<String>  shortUrlCreateCachePenetrationBloomFilter;
    private final LinkGotoMapper linkGotoMapper;
    private final StringRedisTemplate stringRedisTemplate;
    private final RedissonClient redissonClient;
    private final LinkAccessStatsMapper linkAccessStatsMapper;
    private final LinkLocaleStatsMapper linkLocaleStatsMapper;
    private final LinkOsStatsMapper linkOsStatsMapper;
    private final LinkBrowserStatsMapper linkBrowserStatsMapper;
    private final LinkDeviceStatsMapper linkDeviceStatsMapper;
    private final LinkNetworkStatsMapper linkNetworkStatsMapper;
    private final LinkAccessLogsMapper linkAccessLogsMapper;
    private final LinkStatsTodayMapper linkStatsTodayMapper;

    @Value("${short-link.stats.locale.amap-key}")
    private String amapKey;

    @Value("${short-link.domain.default}")
    private String creatShortLinkDefaultDomain;
    /**
     * 创建短链接
     * @param requestParam 创建短链接实体
     * @return 成功构造的短链接返回实体
     */
    @Override
    public LinkCreateRespDTO createLink(LinkCreateReqDTO requestParam) {
        String linkSuffix = generateSuffix(requestParam);
        LinkDO linkDO = BeanUtil.toBean(requestParam, LinkDO.class);
        String fullShortUrl=creatShortLinkDefaultDomain + "/" + linkSuffix;
        linkDO.setDomain(creatShortLinkDefaultDomain);
        linkDO.setFullShortUrl(fullShortUrl);
        linkDO.setShortUrl(linkSuffix);
        linkDO.setEnableStatus(0);
        linkDO.setTotalPv(0);
        linkDO.setTotalUv(0);
        linkDO.setTotalUv(0);

        try {
            linkDO.setIcon( FaviconUtil.getFaviconUrl(linkDO.getOriginUrl()));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }


        LinkGotoDO linkGotoDO = LinkGotoDO.builder()
                .gid(requestParam.getGid())
                .fullShortUrl(fullShortUrl)
                .build();
        //如果相同主键重复插入
        try{
            baseMapper.insert(linkDO);
            //将创建的短链接的gid，fullShortUrl插入短链接跳转数据表link_goto中
            linkGotoMapper.insert(linkGotoDO);
        }catch (DuplicateKeyException ex){
            //判断是否数据库中是否存在相同完整短链接
            LambdaQueryWrapper<LinkDO> queryWrapper= Wrappers.lambdaQuery(LinkDO.class)
                    .eq(LinkDO::getFullShortUrl,linkDO.getFullShortUrl());
            LinkDO hasLinkDO = baseMapper.selectOne(queryWrapper);
            if(hasLinkDO!=null){
                log.warn("短链接：{} 重复入库", linkDO.getFullShortUrl());
                throw new ServiceException("短链接生成重复");

            }
        }
        //缓存预热
        //String.format(GOTO_SHORT_LINK_KEY,fullShortUrl)意思为：将GOTO_SHORT_LINK_KEY中的字符串作为前缀，与fullShortUrl拼接后成为Key
        stringRedisTemplate.opsForValue()
                .set(String.format(GOTO_SHORT_LINK_KEY,fullShortUrl),
                        requestParam.getOriginUrl(),
                        //缓存保留时间，即短链接有效期时间，单位ms
                        LinkUtil.getLinkCacheValidDate(linkDO.getValidDate()),
                        TimeUnit.MICROSECONDS);

        //将完整短链接添加到短链接创建布隆过滤器
        shortUrlCreateCachePenetrationBloomFilter.add(linkDO.getFullShortUrl());
        return LinkCreateRespDTO.builder()
                .fullShortUrl("http://"+linkDO.getFullShortUrl())
                .gid(linkDO.getGid())
                .shortUrl(linkDO.getShortUrl())
                .originUrl(linkDO.getOriginUrl())
                .build();
    }

    /**
     * 生成短链接后缀，即6位随机字符
     * @param requestParam 短链接传入实体
     * @return 生成的短链接后缀
     */
    private String generateSuffix(LinkCreateReqDTO requestParam) {
        int customGenerateCount=0;
        String shortUrl;
        while(true){
            if(customGenerateCount > 10){
                throw new ServiceException("短链接频繁生成");
            }
            String originUrl = requestParam.getOriginUrl();
            //将短链接原始链接加入毫秒数，增大数据复杂性减小碰撞概率
            originUrl+= System.currentTimeMillis();
            shortUrl = HashUtil.hashToBase62(originUrl);
            if(!shortUrlCreateCachePenetrationBloomFilter.contains(requestParam.getDomain()+"/"+shortUrl)){
                break;
            }
            customGenerateCount++;
        }

        return shortUrl;
    }

    /**
     * 短链接分页查询
     * @param requestParam 分页查询传入实体
     *
     */
    @Override
    public IPage<LinkPageRespDTO> pageLink(LinkPageReqDTO requestParam) {
        LambdaQueryWrapper<LinkDO> pageWrapper = Wrappers.lambdaQuery(LinkDO.class)
                .eq(LinkDO::getGid,requestParam.getGid())
                .eq(LinkDO::getEnableStatus,0)
                .eq(LinkDO::getDelFlag,0)
                .orderByDesc(LinkDO::getCreateTime);
        // 查询分页
        IPage<LinkDO> resultPage= baseMapper.selectPage(requestParam,pageWrapper);
        //转换类型
        return resultPage.convert(each->{
            LinkPageRespDTO result=BeanUtil.toBean(each,LinkPageRespDTO.class);
            //为返回的域名添加上http://协议前缀
            result.setDomain("http://"+result.getDomain());
            return result;
        });
    }

    /**
     * 查询分组中短链接数量
     * @param requestParam 查询分组中短链接数量参数
     * @return 查询出的短链接数量响应
     */
    @Override
    public List<LinkGroupQueryRespDTO> groupLinkCount(List<String> requestParam) {
        // TODO 分析lambdaQueryWrapper和QueryWrapper的区别
        // linkCount字段名要和实体类LinkGroupQueryRespDTO中的linkCount相同
        QueryWrapper<LinkDO> queryWrapper = Wrappers.query(new LinkDO())
                .select("gid as gid, count(*) as linkCount")
                .in("gid",requestParam)
                .eq("enable_status",0)
                .eq("del_flag",0)
                .groupBy("gid");
        // List<Map<String, Object>> selectMaps(@Param("ew") Wrapper<T> queryWrapper);所以用map
        List<Map<String,Object>> linkDOList = baseMapper.selectMaps(queryWrapper);
        return BeanUtil.copyToList(linkDOList, LinkGroupQueryRespDTO.class);
    }

    @Override
    public void updateLink(LinkUpdateReqDTO requestParam) {
        LambdaQueryWrapper<LinkDO> queryWrapper = Wrappers.lambdaQuery(LinkDO.class)
                .eq(LinkDO::getFullShortUrl,requestParam.getFullShortUrl())
                .eq(LinkDO::getGid,requestParam.getOriginGid())
                .eq(LinkDO::getDelFlag,0)
                .eq(LinkDO::getEnableStatus,0);

        LinkDO hasLinkDO = baseMapper.selectOne(queryWrapper);
        if(hasLinkDO == null) {
            throw new ClientException("短链接不存在");
        }

        LinkDO linkDO = LinkDO.builder()
                .domain(hasLinkDO.getDomain())
                .shortUrl(hasLinkDO.getShortUrl())
                .clickNum(hasLinkDO.getClickNum())
                .icon(hasLinkDO.getIcon())
                .createdType(hasLinkDO.getCreatedType())
                .fullShortUrl(hasLinkDO.getFullShortUrl())
                .gid(requestParam.getGid())
                .originUrl(requestParam.getOriginUrl())
                .describe(requestParam.getDescribe())
                .validDate(requestParam.getValidDate())
                .validDateType(requestParam.getValidDateType())
                .enableStatus(requestParam.getEnableStatus())
                .build();
        //如果分组gid没有修改
        if (Objects.equals(hasLinkDO.getGid(),requestParam.getGid())){
            LambdaUpdateWrapper<LinkDO> lambdaUpdateWrapper=Wrappers.lambdaUpdate(LinkDO.class)
                    .eq(LinkDO::getFullShortUrl,requestParam.getFullShortUrl())
                    .eq(LinkDO::getGid,requestParam.getGid())
                    .eq(LinkDO::getDelFlag,0)
                    .eq(LinkDO::getEnableStatus,0)
                    .set(Objects.equals(requestParam.getValidDateType(), VailDateTypeEnum.PERMANENT.getType()) , LinkDO::getValidDate,null);


            baseMapper.update(linkDO,lambdaUpdateWrapper);
        }else {
            //修改短链接数据表中gid
            LambdaUpdateWrapper<LinkDO> lambdaUpdateWrapper=Wrappers.lambdaUpdate(LinkDO.class)
                    .eq(LinkDO::getFullShortUrl,requestParam.getFullShortUrl())
                    .eq(LinkDO::getGid,hasLinkDO.getGid())
                    .eq(LinkDO::getDelFlag,0)
                    .eq(LinkDO::getEnableStatus,0)
                    .set(Objects.equals(requestParam.getValidDateType(), VailDateTypeEnum.PERMANENT.getType()) , LinkDO::getValidDate,null);
            baseMapper.delete(lambdaUpdateWrapper);
            baseMapper.insert(linkDO);
            //修改跳转表中的gid
            LinkGotoDO linkGotoDO = new LinkGotoDO();
            linkGotoDO.setGid(linkDO.getGid());
            LambdaUpdateWrapper<LinkGotoDO> linkGotoDOLambdaUpdateWrapper = Wrappers.lambdaUpdate(LinkGotoDO.class)
                    .eq(LinkGotoDO::getFullShortUrl,requestParam.getFullShortUrl());
            linkGotoMapper.update(linkGotoDO,linkGotoDOLambdaUpdateWrapper);
        }


    }

    /**
     * 短链接跳转
     * @param shortUrl 短链接
     * @param request 请求信息
     * @param response 构建和发送响应
     * 雪崩关键词是同一时间大批量数据失效
     * 穿透关键字是非正常数据请求
     * 击穿关键词是并发查询同一数据
     */
    @SneakyThrows
    @Override
    public void restoreUrl(String shortUrl, ServletRequest request, ServletResponse response) {
        String serverName = request.getServerName();
        // TODO 分析短链接默认域名创建代码
        String serverPort = Optional.of(request.getServerPort())
                .filter(each->!Objects.equals(each,80))
                .map(String::valueOf)
                .map(each->":"+each)
                .orElse("");
        String fullShortLink = serverName+serverPort+"/"+shortUrl;
        // Key值为fullShortLink，value为originLink
        String originLink= stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortLink));
        if(StrUtil.isNotBlank(originLink)){
            //记录监控信息
            linkStats(fullShortLink,null,request,response);
            //sendRedirect 方法用于将客户端浏览器重定向到指定的 URL。它会发送一个特殊的 HTTP 响应状态码（302 Found）和一个新的 URL，告诉浏览器去请求该 URL。浏览器会自动跳转到该 URL，并显示新的页面。
            ((HttpServletResponse) response).sendRedirect(originLink);
            return;
        }

        //解决缓存穿透问题
        boolean contains =shortUrlCreateCachePenetrationBloomFilter.contains(fullShortLink);
        if (!contains){
            //短链接重定向，页面不存在
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }
        String gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY,fullShortLink));
        if (StrUtil.isNotBlank(gotoIsNullShortLink)){
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }


        //如果缓存中Key刚好失效或被删除的同时，有大量请求访问该Key，就会造成缓存击穿，此时大量请求直接访问数据库，此时要用分布式锁解决
        //如果没有设置过期时间，不会发生缓存击穿
        RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortLink));
        lock.lock();
        try {
            //双重判定锁，如果缓存中已有数据，则直接返回。（第一次加锁后放入缓存，后续等待的请求获取锁后，返现缓存中已有数据，不需要再访问数据库
            originLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY,fullShortLink));
            if (StrUtil.isNotBlank(originLink)){
                linkStats(fullShortLink,null,request,response);
                ((HttpServletResponse) response).sendRedirect(originLink);
                return;
            }
            // 如果缓存中不存在原始链接，查询数据库获取跳转实体对象
            LambdaQueryWrapper<LinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(LinkGotoDO.class)
                    .eq(LinkGotoDO::getFullShortUrl,fullShortLink);
            LinkGotoDO linkGotoDO= linkGotoMapper.selectOne(linkGotoQueryWrapper);
            if(linkGotoDO == null){
                //数据不存在时，为了防止缓存穿透，用缓存空对象的方法，防止相同请求频繁查询数据库
                stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY,fullShortLink),"-",30, TimeUnit.MINUTES);
                // 严谨来说此处需要风控
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                return;
            }
            LambdaQueryWrapper<LinkDO> queryWrapper = Wrappers.lambdaQuery(LinkDO.class)
                    .eq(LinkDO::getGid,linkGotoDO.getGid())
                    .eq(LinkDO::getFullShortUrl,fullShortLink)
                    .eq(LinkDO::getDelFlag,0)
                    .eq(LinkDO::getEnableStatus,0);
            LinkDO linkDO = baseMapper.selectOne(queryWrapper);
            //移至回收站之后，再次判断时查询不到已回收的数据，也要缓存空对象
            if(linkDO == null||(linkDO.getValidDate() != null &&linkDO.getValidDate().before(new Date()))){
                //如果有效期为不为空并且有效期在当前时间之前，则缓存空对象,有效期为空时是永久链接,如果数据库中没有该实体数据，也缓存为空
                stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY,fullShortLink),"-",30, TimeUnit.MINUTES);
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                return;
            }
                // 设置第一个请求获取到锁后记录下来，后续相同请求全都没有必要继续执行
            stringRedisTemplate.opsForValue().set(
                    String.format(GOTO_SHORT_LINK_KEY, fullShortLink),
                    linkDO.getOriginUrl(),
                    //缓存设置的时间跟有效期相对应的，有效期过了缓存也肯定会失效
                    LinkUtil.getLinkCacheValidDate(linkDO.getValidDate()),TimeUnit.MICROSECONDS);
            linkStats(fullShortLink,linkGotoDO.getGid(),request,response);
            ((HttpServletResponse) response).sendRedirect(linkDO.getOriginUrl());


        }finally {
            lock.unlock();
        }
    }

    /**
     * 短链接监控信息
     * @param fullShortUrl 完整短链接
     * @param gid 分组标识
     * @param request 请求
     * @param response 响应
     */
    private void linkStats(String fullShortUrl,String gid,ServletRequest request, ServletResponse response){
        // 单线程操作没有并发安全问题，这里使用AtomicXXX与线程安全无关。
        // 由于使用了Lambda表达式，所以要使用AtomicXXX，函数表达式里不允许对对象再次赋值，只能用 final 类型的，这里相当于取巧了
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        Cookie[] cookies = ((HttpServletRequest) request).getCookies();


        // TODO new data 替换
        try{
            //创建一个线程安全的可变引用对象uv，它的泛型参数是String
            AtomicReference<String> uv = new AtomicReference<>();
            Runnable addResponseCookieTask = ()->{
                uv.set( UUID.fastUUID().toString());
                Cookie uvCookie = new Cookie("uv",uv.get());
                //设置cookie的最大存活时间为30天
                uvCookie.setMaxAge(60*60*24*30);
                //通过将字符串的长度作为结束位置，可以截取从斜杠（"/"）的位置开始，一直到字符串末尾的子字符串。
                uvCookie.setPath(StrUtil.sub(fullShortUrl,fullShortUrl.indexOf("/"),fullShortUrl.length()));
                ((HttpServletResponse) response).addCookie(uvCookie);
                uvFirstFlag.set(Boolean.TRUE);
                // TODO 似乎没有设置缓存时间
                stringRedisTemplate.opsForSet().add("short-link:stats:uv:"+fullShortUrl,uv.get());
            };
            //统计短链接uv访问
            if(ArrayUtil.isNotEmpty(cookies)){
                Arrays.stream(cookies)
                        .filter(each->Objects.equals(each.getName(),"uv"))
                        .findFirst()
                        .map(Cookie::getValue)
                        .ifPresentOrElse(each->{
                            uv.set(each);
                            Long uvAdded = stringRedisTemplate.opsForSet().add("short-link:stats:uv:"+fullShortUrl,each);
                            uvFirstFlag.set(uvAdded !=null&&uvAdded >0L);
                        },addResponseCookieTask);
            }else {
                addResponseCookieTask.run();
            }

            //统计短链接uip访问
            String remoteAddr = request.getRemoteAddr();
            Long uipAdded = stringRedisTemplate.opsForSet().add("short-link:stats:uv:"+fullShortUrl,remoteAddr);
            boolean uipFirstFlag = uipAdded!=null&&uipAdded>0L;



            if(StrUtil.isBlank(gid)){

                LambdaQueryWrapper<LinkGotoDO> queryWrapper = Wrappers.lambdaQuery(LinkGotoDO.class)
                        .eq(LinkGotoDO::getFullShortUrl,fullShortUrl);
                LinkGotoDO linkGotoDO = linkGotoMapper.selectOne(queryWrapper);
                gid = linkGotoDO.getGid();
            }
            int hour = DateUtil.hour(new Date(),true);
            Week week = DateUtil.dayOfWeekEnum(new Date());
            //ISO 8601还提供了一些扩展功能，用于表示更复杂的日期和时间场景，例如:
            //带毫秒的时间表示（HH:MM:SS.sss）；
            //时间间隔表示，如持续时间（P1Y2M3DT4H5M6S）；
            //周次表示，如年份和周数的组合（YYYY-Www）。
            int weekValue = week.getIso8601Value();
            LinkAccessStatsDO linkAccessStatsDO = LinkAccessStatsDO.builder()
                    .pv(1)
                    .uv(uvFirstFlag.get() ? 1:0)
                    .uip(uipFirstFlag ? 1:0)
                    .hour(hour)
                    .weekday(weekValue)
                    .fullShortUrl(fullShortUrl)
                    .gid(gid)
                    .date(new Date())
                    .build();
            linkAccessStatsMapper.setLinkAccessStats(linkAccessStatsDO);

            //获取请求的地区信息并存入数据库

            Map<String,Object> localParamMap = new HashMap<>();
            localParamMap.put("key",amapKey);
            localParamMap.put("ip" ,remoteAddr);
            String localResultStr = HttpUtil.get(AMAP_REMOTE_URL,localParamMap);
            JSONObject localResultObj= JSON.parseObject(localResultStr);
            String infoCode = localResultObj.getString("infocode");
            if(StrUtil.isNotBlank(infoCode) && StrUtil.equals(infoCode,"10000")){
                String province = localResultObj.getString("province");
                boolean unknownFlag = StrUtil.equals(province,"[]");
                LinkLocaleStatsDO linkLocaleStatsDO = LinkLocaleStatsDO.builder()
                        .province(unknownFlag ? "未知":province)
                        .city(unknownFlag ? "未知":localResultObj.getString("city"))
                        .adcode(unknownFlag ? "未知":localResultObj.getString("adcode"))
                        .cnt(1)
                        .country("中国")
                        .fullShortUrl(fullShortUrl)
                        .gid(gid)
                        .date(new Date())
                        .build();
                linkLocaleStatsMapper.setLinkLocaleStats(linkLocaleStatsDO);

                //记录用户设备信息
                LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                        .os(LinkUtil.getOs((HttpServletRequest) request))
                        .fullShortUrl(fullShortUrl)
                        .gid(gid)
                        .date(new Date())
                        .cnt(1)
                        .build();
                linkOsStatsMapper.setLinkOsStats(linkOsStatsDO);
                //记录用户浏览器信息
                LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                        .browser(LinkUtil.getBrowser(((HttpServletRequest) request)))
                        .cnt(1)
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .date(new Date())
                        .build();
                linkBrowserStatsMapper.setLinkBrowserStats(linkBrowserStatsDO);


                //记录访问设备
                LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                        .device(LinkUtil.getDevice(((HttpServletRequest) request)))
                        .cnt(1)
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .date(new Date())
                        .build();
                linkDeviceStatsMapper.shortLinkDeviceState(linkDeviceStatsDO);
                //记录访问网络类型
                LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
                        .network(LinkUtil.getNetwork(((HttpServletRequest) request)))
                        .cnt(1)
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .date(new Date())
                        .build();
                linkNetworkStatsMapper.shortLinkNetworkState(linkNetworkStatsDO);
                //记录高频访问ip（用户访问日志）
                LinkAccessLogsDO linkAccessLogsDO =LinkAccessLogsDO.builder()
                        .fullShortUrl(fullShortUrl)
                        .gid(gid)
                        //TODO 将多个相同对象抽象成同一个对象
                        .user(uv.get())
                        .device(LinkUtil.getDevice(((HttpServletRequest) request)))
                        .network(LinkUtil.getNetwork(((HttpServletRequest) request)))
                        .locale(StrUtil.format("中国",unknownFlag ? "未知":localResultObj.getString("city")))
                        .browser(LinkUtil.getBrowser(((HttpServletRequest) request)))
                        .os(LinkUtil.getOs((HttpServletRequest) request))
                        .ip(remoteAddr)
                        .delFlag(0)
                        .build();
                linkAccessLogsMapper.insert(linkAccessLogsDO);
                baseMapper.incrementStats(gid,fullShortUrl,1,uvFirstFlag.get() ? 1:0,uipFirstFlag ? 1:0);
                LinkStatsTodayDO linkStatsTodayDO = LinkStatsTodayDO.builder()
                        .gid(gid)
                        .fullShortUrl(fullShortUrl)
                        .date(new Date())
                        .todayPv(1)
                        .todayUv(uvFirstFlag.get() ? 1:0)
                        .todayUip(uipFirstFlag ? 1:0)
                        .build();
                linkStatsTodayMapper.setLinkStatsToday(linkStatsTodayDO);
            }


        }catch (Throwable ex){
            log.error("短链接访问量统计异常",ex);
        }
    }

    /**
     * 获取网站标题
     * @param url 链接地址
     * @return 网站标题
     */
    @Override
    public String getLinkTitle(String url) {
        try {
            Document document = Jsoup.connect(url).get();
            return document.title();
        } catch (Exception e) {
            // 处理异常情况，例如无法连接到网站等
            throw new ClientException(TARGET_WEBSITE_TITLE_ERROR);
        }
    }
}
